package org.fhnw.aigs.server.communication;
import org.fhnw.aigs.server.common.ServerConfiguration;
import org.fhnw.aigs.commons.communication.*;
import java.io.*;
import java.net.*;
import java.util.logging.*;
import javax.xml.parsers.*;
import org.fhnw.aigs.commons.*;
import org.fhnw.aigs.server.common.LogRouter;
import org.fhnw.aigs.server.common.LoggingLevel;
import org.fhnw.aigs.server.gameHandling.GameManager;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.fhnw.aigs.server.gameHandling.*;
/**
* This class is responsible for the message flow. One ServerMessageBroker is
* assigned to every client connection. The incoming messages will be parsed and
* analyzed. System messages such as {@link ClientClosedMessage}s will be
* handled in this class. All other messages will be forwarded to the game
* assigned to the game connected to the ServerMessageBroker.<br>
* v1.0 Initial release<br>
* v1.1 Functional changes<br>
* v1.1.1 Minor changes (due to changes in other classes)<br>
* v1.2 Changing of logging
*
* @author Matthias Stöckli (v1.0)
* @version 1.2 (Raphael Stoeckli, 24.02.2015)
*/
public class ServerMessageBroker implements Runnable {
/**
* The socket which connects the server to the client.
*/
private Socket socket;
/**
* The BufferedReader used to read the incoming messages.
*/
private BufferedReader in;
/**
* The game to which this connection is connected to.
*/
private Game game;
/**
* The player using this connection.
*/
private Player player;
/**
* A flag that shows whether this game has already been initialized or not.
*/
private boolean isGameInitialized;
/**
* The currently received message in a human readable form.
*/
private static String currentPrettyPrintedMessage = "";
/**
* Indicates whether the currently processed message is a message which is
* not related to games, e.g. a JoinMessage etc.
*/
private boolean nonGameMessageReceived = false;
/**
* A flag that indicates whether the current connection is still open, this
* variable can help tracking exceptions.
*/
private boolean connectionOpen = true;
/**
* Initializes the ServerMessageBroker and sets up a {@link Socket}.<br>
* This class is responsible for parsing, interpreting and passing messages
* to the game logic (client wise). This class is the counterpart of the
* {@link org.fhnw.aigs.client.communication.ClientMessageBroker} on the server side.
*
* @param socket Socket which connects the client to the server.
*/
public ServerMessageBroker(Socket socket) throws IOException {
this.socket = socket;
this.in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
}
/**
* Starts the message listening loop.
*/
@Override
public void run() {
//LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.INFO, "Server ready, waiting for incoming messages...");
LogRouter.log(ServerMessageBroker.class.getName(), LoggingLevel.info, "Server ready, waiting for incoming messages...");
listenForMessages();
}
/**
* This method constantly listens for new inputs coming from the client. The
* messages are first parsed using {@link ServerMessageBroker#parseInput}
* Then the parsed messages will be handled. System messages will be handled
* by {@link ServerMessageBroker#checkForNonGameMessages}. All other
* messages are then passed to the
* {@link org.fhnw.aigs.commons.Game#processGameLogic} method.
*/
private void listenForMessages() {
String inputString = "";
try {
while (connectionOpen == true && (inputString = in.readLine()) != null) {
Message parsedMessage = parseInput(inputString);
if (parsedMessage instanceof KeepAliveMessage == false) {
//printMessage(inputString);
LogRouter.log(ServerMessageBroker.class.getName(), LoggingLevel.info, "<= {0}", inputString);
}
checkForNonGameMessages(parsedMessage);
// Stop the processing if a non game message was
// handled or if there is no game or the message is not valid.
if (nonGameMessageReceived || game == null) {
nonGameMessageReceived = false;
continue;
}
// Check for any kind of exception. In the case of an exception
// the game will be terminated and the clients will be informed.
try {
game.processGameLogic(parsedMessage, player);
game.checkForWinningCondition();
}
catch (StackOverflowError stackOverFlow) { // If the game throws an Exception, inform the clients.
// Handle StackOverFlowErrors
GameManager.terminateGame(game, player, "An error (something really bad) occured.");
ForceCloseMessage forceCloseMessage = new ForceCloseMessage("You caused a StackOverflowError (something really bad).");
forceCloseMessage.send(socket, player);
socket.close();
in.close();
connectionOpen = false;
}
catch (Exception ex) {
ExceptionMessage exceptionMessage = new ExceptionMessage(ex);
exceptionMessage.send(socket, player);
GameManager.terminateGame(game, player, "An exception occured.");
socket.close();
in.close();
connectionOpen = false;
//LOGLogger.getLogger(ServerMessageBroker.class.getName()).log(Level.SEVERE, "An exception in the game forced the server to close the following game: " + game.toString(), ex);
LogRouter.log(ServerMessageBroker.class.getName(), LoggingLevel.severe, "An exception in the game forced the server to close the following game: " + game.toString(), ex);
}
}
} catch (IOException ex) {
GameManager.terminateGame(game, player, "An I/O exception occured.");
try {
socket.close();
in.close();
connectionOpen = false;
} catch (IOException ex2) {
//LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.SEVERE, "The connection could not be closed", ex2);
LogRouter.log(ServerMessageBroker.class.getName(), LoggingLevel.severe, "The connection could not be closed", ex2);
}
}
catch (Exception ex) // All other exceptions
{
GameManager.terminateGame(game, player, "An unknown exception occured.");
try {
socket.close();
in.close();
connectionOpen = false;
} catch (IOException ex2) {
//LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.SEVERE, "The connection could not be closed", ex2);
LogRouter.log(ServerMessageBroker.class.getName(), LoggingLevel.severe, "The connection could not be closed", ex2);
}
}
}
/**
* This method parses the incoming messages.<br>
* It first tries to create a DOMDocument in order to check whether the
* string is valid xml and to determine the message type. If the message
* cannot be parsed, the client will receive a {@link BadInputMessage}. If
* the parsing is successful, the respective message class is loaded via the
* game's ClassLoader, see {@link GameLoader}.<br>
* In a last step, the {@link Message#parse} method will take care of the
* parsing.
*
* @param inputString The message as received from the clients.
* @return The parsed message.
*/
private Message parseInput(String inputString) {
// Used to create an input source and for parsing.
StringReader reader = new StringReader(inputString);
String messageClassPath = extractMessageClassPath(new InputSource(reader), inputString);
// Recreate the reader because the reader was read with the last operation
reader = new StringReader(inputString);
// If the game has not yet been initialized, use the System class loader
// to get to the messages in AIGS commons otherwise use the classloader
// of the current game.
ClassLoader loader = getClassLoader();
// Try to load and parse the Message Class dynamically by the loginName provided
// by the attribute "classPath" which is part of every message.
try {
Class messageClass = Class.forName(messageClassPath, true, loader);
return Message.parse(reader, messageClass);
} catch (ClassNotFoundException ex) {
//LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.SEVERE, "Could not find a matching class. Check the package name, @XmlElement annotations and the jars.", ex);
LogRouter.log(ServerMessageBroker.class.getName(), LoggingLevel.waring, "Could not find a matching class. Check the package name, @XmlElement annotations and the jars.", ex);
return null;
}
catch (Exception ex) // All other exceptions
{
//LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.SEVERE, "An unknown error occurred.", ex);
LogRouter.log(ServerMessageBroker.class.getName(), LoggingLevel.waring, "An unknown error occurred.", ex);
return null;
}
}
/**
* Uses a {@link org.w3c.dom.Document} to get the attribute "FullyQualifiedClassName"
* of a message. The value of this attribute is inherent to every message.
* By extracting it, messages can be instantiated by using reflection.
*
* @param inputSource The incoming message as an InputSource
* @param inputString The incoming message as an InputString (for exception
* handling purposes only)
* @return The value of the attribute "FullyQualifiedClassName".
*/
private String extractMessageClassPath(InputSource inputSource, String inputString) {
// Using a DOMDocument we can extract an attribute.
Document DOMDocument;
try {
DOMDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSource);
// Read the attribute "classPath" of the sent XML. It represents the qualified loginName of the class
// e.g. "org.fhnw.aigs.commons.TicTacToe.FieldClickMessage" which points to the class "FieldClickMessage".
return DOMDocument.getDocumentElement().getAttribute("FullyQualifiedClassName");
} catch (ParserConfigurationException | SAXException | IOException ex) {
//LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.SEVERE, "Could not parse input into xml format." + "Client sent the following string: \n{0}", inputString);
LogRouter.log(ServerMessageBroker.class.getName(), LoggingLevel.waring, "Could not parse input into xml format." + "Client sent the following string: \n{0}", inputString);
BadInputMessage badInputMessage = new BadInputMessage(inputString);
badInputMessage.send(socket, player);
return null;
}
catch (Exception ex) // All other Exceptions
{
//LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.SEVERE, "An unknonw error occurred.", ex);
LogRouter.log(ServerMessageBroker.class.getName(), LoggingLevel.waring, "An unknonw error occurred.", ex);
BadInputMessage badInputMessage = new BadInputMessage(inputString);
badInputMessage.send(socket, player);
return null;
}
}
/**
* Gets the {@link ClassLoader} of a game.
*
* @return Returns the ClassLoader of a game if there is one, otherwise the
* SystemClassLoader is returned.
*/
private ClassLoader getClassLoader() {
if (isGameInitialized) {
return GameLoader.getClassLoaderByName(game.getGameName());
} else {
return ClassLoader.getSystemClassLoader();
}
}
/**
* Checks the incoming messages for the following system messages:<br><ul>
* <li>{@link IdentificationMessage}</li>
* <li>{@link ClientClosedMessage}</li>
* <li>{@link JoinMessage}</li>
* <li>{@link KeepAliveMessage}</li>
* </ul>
* The messages are then handled individually.
*
* @param parsedMessage The parsed message.
*/
private void checkForNonGameMessages(Message parsedMessage) {
Message m = parsedMessage;
if (m instanceof IdentificationMessage) {
handledentificationMessage(parsedMessage);
} else if (m instanceof ClientClosedMessage) {
handleClientClosedMessage(parsedMessage);
} else if (parsedMessage instanceof JoinMessage) {
handleJoinMessage(parsedMessage);
} else if (parsedMessage instanceof KeepAliveMessage) {
handleKeepAliveMessage(parsedMessage);
}
}
/**
* Checks whether the {@link IdentificationMessage} received from a client
* is valid. Then do the necessary steps to authentificate the user.
*
* @param parsedMessage
*/
private void handledentificationMessage(Message parsedMessage) {
IdentificationMessage identificationMessage = (IdentificationMessage) parsedMessage;
String loginName = identificationMessage.getLoginName();
String password = identificationMessage.getPassword();
String playerName = identificationMessage.getPlayerName();
// Checks whether the server is running as local host or if the option
// "IsMultiLoginAllowed" is set to true.
boolean isMultiLoginAllowed;
String ipAddress = socket.getRemoteSocketAddress().toString();
if (ipAddress.startsWith("/127.0.0.1") || ServerConfiguration.getInstance().getIsMultiLoginAllowed()) {
isMultiLoginAllowed = true;
} else {
isMultiLoginAllowed = false;
}
// Identify the user and create a player based on the message.
IdentificationResponseMessage identificationResponseMessage = User.identify(loginName, password, playerName, isMultiLoginAllowed);
this.player = new Player(identificationResponseMessage.getLoginName(), identificationResponseMessage.getPlayerName(), false);
this.player.setSocket(socket);
identificationResponseMessage.send(socket, player);
nonGameMessageReceived = true; // Was handled
}
/**
* If the client closed the window (or the client was closed out of another
* reason), this method closes the connection, logs off the user and
* terminates the games he or she was in.
*
* @param parsedMessage The parsed message.
*/
private void handleClientClosedMessage(Message parsedMessage) {
ClientClosedMessage clientClosedMessage = (ClientClosedMessage) parsedMessage;
try {
socket.close();
in.close();
//LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.INFO, "Connection successfully closed.");
LogRouter.log(ServerMessageBroker.class.getName(), LoggingLevel.info, "Connection successfully closed.");
} catch (IOException ex) {
//LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.SEVERE, null, ex);
LogRouter.log(ServerMessageBroker.class.getName(), LoggingLevel.severe, null, ex);
}
GameManager.terminateGame(game, player, clientClosedMessage.getReason());
connectionOpen = true;
nonGameMessageReceived = true;
}
/**
* Handles a join request. When a user wants to start/initialize a game, he
* or she will send a JoinMessage. The method will call
* {@link GameManager#joinGame}.
*
* @param parsedMessage The {@link JoinMessage}.
*/
private void handleJoinMessage(Message parsedMessage) {
JoinMessage joinMessage = (JoinMessage) parsedMessage;
// Ignore JoinMessages if there already is a game going on.
if (isGameInitialized == false) {
game = GameManager.joinGame(joinMessage, player, joinMessage.getPartyName());
if (game == null) {
connectionOpen = false;
} else {
isGameInitialized = true;
}
}
nonGameMessageReceived = true;
}
/**
* Handles the client's answers of {@link KeepAliveMessage}s. The response
* is simply forwarded to the {@link KeepAliveManager}.
*
* @param parsedMessage The {@link KeepAliveMessage}.
*/
private void handleKeepAliveMessage(Message parsedMessage) {
KeepAliveMessage keepAliveMessage = (KeepAliveMessage) parsedMessage;
KeepAliveManager.handleResponse(keepAliveMessage);
nonGameMessageReceived = true;
}
}